package org.bhf.service;

/**
 * Copyright (c) 2007, Blue Hole Software. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */

import org.bhf.service.view.NullProcessView;
import org.bhf.util.TriValueBoolean;
import org.bhf.util.AnnotationAccess;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

/**
 * Tag annotation to identify a Service or <code>Component</code>method.
 */
@Documented
@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.METHOD)
public @interface ServiceMethod
{
    /**
     * Utility accessors for <code>ServiceMethod</code>
     * @exclude
     */
    public static final class Util
    {
        static final Class<ServiceMethod> clazz = ServiceMethod.class;
        static final String     INHERIT_ROLES   = "_inherit_";
        static final String     ANY_ROLES       = "ANY";
        static final Class[]    THREE_ARG       = new Class[] { Object.class, Method.class, Object.class };

        /**
         * Return <code>true</code> is this is a Service Method.
         * @param method the target <code>Class</code>.
         * @return <code>true</code> is this is a Service Method.
         */
        public static boolean isServiceMethod( final Method method )
        {
            return AnnotationAccess.getAnnotation( method, clazz ) != null;
        }

        /**
         * Return <code>true</code> is this is a Service Method.
         * @param method the target <code>Class</code>.
         * @return <code>true</code> is this is a Service Method.
         */
        public static boolean       isCrossSiteMethod( final Method method )
        {
            final Service       sc  = Service.Util.get( method.getDeclaringClass() );
            final ServiceMethod sm  = AnnotationAccess.getAnnotation( method, clazz );
                  boolean       xs  = false;

            if( sc != null )
                xs = sc.crossSite();
            if( sm != null && sm.crossSite() != TriValueBoolean.INHERIT )
                xs = sm.crossSite() == TriValueBoolean.TRUE;

            return xs;
        }

        /**
         * Return <code>true</code> is this is a Service Method.
         * @param method the target <code>Class</code>.
         * @param service The service object
         * @param result The result of the method call
         * @return <code>ProcessView</code>
         */
        public static ProcessView   getView( final Object service, final Method method, final Object result )
        {
            final Service                       sc      = Service.Util.get( method.getDeclaringClass() );
            final ServiceMethod                 sm      = AnnotationAccess.getAnnotation( method, clazz );
                  Class<? extends ProcessView>  clazz   = sm.view();

            if( clazz == NullProcessView.class && sc != null )
            {
                clazz   = sc.view();
            }

            return newProcessView( clazz, method, service, result );
        }

        private static ProcessView  newProcessView( final Class<? extends ProcessView> clazz, final Method method, final Object service, final Object result )
        {
            try
            {
                final Constructor<? extends ProcessView> ctor = clazz.getConstructor( THREE_ARG );
                return ctor.newInstance( service, method, result );
            }
            catch( final NoSuchMethodException e )
            {
                throw new RuntimeException( "Unable to find three arg ProcessView ctor for " + clazz.getName() );
            }
            catch( final Exception e )
            {
                final RuntimeException re = new RuntimeException( "Failure to instantiate a ProcessView" );
                re.initCause( e );
                throw re;
            }
        }

        /**
         * Return <code>true</code> iff the method is accessible by the given role
         * @param method Service method
         * @param role Role name
         * @return <code>true</code> iff the method is accessible by the given role
         */
        public static boolean   hasAccess( final Method method, final String role )
        {
            final Service           sc      = Service.Util.get( method.getDeclaringClass() );
            final ServiceMethod     sm      = AnnotationAccess.getAnnotation( method, clazz );

            for( final String allowedRole : sm.roles() )
            {
                if( Util.INHERIT_ROLES.equals( allowedRole ) )
                {
                    for( final String allowedRoleForService : sc.roles() )
                    {
                        if( ANY_ROLES.equals( allowedRoleForService ) || role.equals( allowedRoleForService ) )
                        {
                            return true;
                        }
                    }
                }
                else if( ANY_ROLES.equals( allowedRole ) || role.equals( allowedRole ) )
                {
                    return true;
                }
            }

            return false;
        }
    }

    /**
     * Default <code>ProcessView</code> for method return results. The default is <code>JSONProcessView</code>.
     * @return Default <code>ProcessView</code> for method return results.
     */
    Class<? extends ProcessView>    view() default NullProcessView.class;

    /**
     * A list of roles, by name, that are allowed to access this method, overriding any roles specified at the
     * service level. The special role name "ANY" implies all roles.
     * @return A list of roles, by name, that are allowed to access this method.
     */
    String[]                        roles() default Util.INHERIT_ROLES;

    /**
     * If <code>true</code>, allow cross site invocations of this method, the default is false unless otherwise
     * specified in the <code>Service</code> annotation. Cross site methods are vunerable to
     * Cross Site Request Forgery (CSRF) attacks and must nver depend, directly or indirectly, on cookies for security.
     * @return <code>true</code>, allow cross site invocations of this method.
     */
    TriValueBoolean                 crossSite() default TriValueBoolean.INHERIT;
}
